package ranab.server.ftp.usermanager;

import java.io.File;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Properties;
import java.util.Collections;
import javax.naming.NamingException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.SearchResult;
import javax.naming.directory.ModificationItem;

import ranab.io.LogFile;
import ranab.server.ftp.FtpConfig;

/**
 * Ldap based user manager class. Tested using Netscape Directory Server 4.1.
 * The LDAP requires the password to be nonempty for simple authentication. So
 * instead of using empty string password (""), we will be using single space (" ").
 * 
 * @author <a href="mailto:rana_b@yahoo.com">Rana Bhattacharyya</a>
 */
public
class LdapUserManager extends UserManager {
    
    
    // LDAP attributes
	private final static String LOGIN      = "memberuid";
    private final static String UID        = "uid";
    private final static String CN         = "cn";
    private final static String SN         = "sn";
    private final static String PASSWORD   = "userpassword";
    private final static String OBJ_CLASS  = "objectclass";
    private final static String ENABLE     = "enableflag";
    private final static String ROOT_DIR   = "homedirectory";
    private final static String WRITE_PERM = "writepermission";
    private final static String IDLE_TIME  = "idletime";
    private final static String UP_RATE    = "uploadrate";
    private final static String DOWN_RATE  = "downloadrate";
	
    private final static String[] ALL_ATTRS = {
    	CN,
    	LOGIN,
        ENABLE,
        ROOT_DIR,
        WRITE_PERM,
        IDLE_TIME,
        UP_RATE,
        DOWN_RATE
    };
    
    private final static Attribute OBJCLASS_ATTR = new BasicAttribute(OBJ_CLASS, true);
    
    
    // Currently we are using only one connection.
    // This will be replaced by LDAP connection pool. 
    private DirContext mAdminContext;
    private Properties mAdminEnv;
	private String mstLdapRoot;
    
	
    /**
     * Instantiate <code>UserManager</code> implementation.
     * Open LDAP connection pool.
     */
    public LdapUserManager(FtpConfig cfg) throws Exception { 
    	super(cfg);
        
        // get ldap parameters
        String url      = cfg.getProperty(FtpConfig.PREFIX + "ldap.url");
        String ldapRoot = cfg.getProperty(FtpConfig.PREFIX + "ldap.root");
        String admin    = cfg.getProperty(FtpConfig.PREFIX + "ldap.admin");
        String password = cfg.getProperty(FtpConfig.PREFIX + "ldap.password");
        String auth     = cfg.getProperty(FtpConfig.PREFIX + "ldap.authentication");
        
        mAdminEnv = new Properties();
        mAdminEnv.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        mAdminEnv.setProperty(Context.PROVIDER_URL, url);
        mAdminEnv.setProperty(Context.SECURITY_AUTHENTICATION, auth);             
        mAdminEnv.setProperty(Context.SECURITY_PRINCIPAL, admin);             
        mAdminEnv.setProperty(Context.SECURITY_CREDENTIALS, password);             
                     
        mAdminContext = new InitialDirContext(mAdminEnv);
        mstLdapRoot = ldapRoot;
        getConfig().getLogger().info("LDAP user manager opened.");
    }
    
    
    /**
     * Get common name
     */
    private synchronized String getCommonName(String login) throws NamingException {
    	Attributes matchAttrs = new BasicAttributes(true);
		matchAttrs.put(new BasicAttribute(LOGIN, login));
        matchAttrs.put(OBJCLASS_ATTR);
        NamingEnumeration answers = mAdminContext.search("ou=people," + mstLdapRoot, matchAttrs, ALL_ATTRS);
		
        String cn = null;
        if (answers.hasMore()) {
        	SearchResult sr = (SearchResult)answers.next();
            cn = sr.getAttributes().get(CN).get().toString();
        }
        answers.close();
        return cn;
    }
    
    
    /**
     * Get all user names
     */
    public synchronized Collection getAllUserNames() {
        ArrayList allUsers = new ArrayList();
        
        try {
        	Attributes matchAttrs = new BasicAttributes(true);
	        matchAttrs.put(OBJCLASS_ATTR);
	        NamingEnumeration answers = mAdminContext.search("ou=people," + mstLdapRoot, matchAttrs, ALL_ATTRS);
	        while (answers.hasMore()) {
	        	SearchResult sr = (SearchResult)answers.next();
	            String login = sr.getAttributes().get(LOGIN).get().toString();
	        	allUsers.add(login);
            }
        }
        catch(NamingException ex) {
        	getConfig().getLogger().error(ex);
        }
        
        Collections.sort(allUsers);
        return allUsers;
    } 
    
    
    /**
     * Get user object.
     */
    public synchronized User getUserByName(String name) {
    	User user = null;
        
        try {
        	Attributes matchAttrs = new BasicAttributes(true);
            matchAttrs.put(new BasicAttribute(LOGIN, name));
	        matchAttrs.put(OBJCLASS_ATTR);
	        NamingEnumeration answers = mAdminContext.search("ou=people," + mstLdapRoot, matchAttrs, ALL_ATTRS);
        	if (answers.hasMore()) {
        		SearchResult sr = (SearchResult)answers.next();
                Attributes attrs = sr.getAttributes();
                        
                user = new User();
                user.setName(attrs.get(LOGIN).get().toString());
                user.getVirtualDirectory().setRootDirectory(new File(attrs.get(ROOT_DIR).get().toString()));
            	user.setEnabled(Boolean.TRUE.toString().equals(attrs.get(ENABLE).get().toString()));
                user.getVirtualDirectory().setWritePermission(Boolean.TRUE.toString().equals(attrs.get(WRITE_PERM).get().toString()));  
            	user.setMaxIdleTime( Integer.parseInt(attrs.get(IDLE_TIME).get().toString()) );
                user.setMaxUploadRate( Integer.parseInt(attrs.get(UP_RATE).get().toString()) );
                user.setMaxDownloadRate( Integer.parseInt(attrs.get(DOWN_RATE).get().toString()) );
            }
            answers.close();
        }
        catch(NamingException ex) {
        	getConfig().getLogger().error(ex);
            user = null;
        }
        
        return user;
    }
    
    
    /**
     * User authentication.
     */
    public boolean authenticate(String name, String password) {
        
        // empty password string is not allowed
        if (password == null) {
        	password = " ";
        }
        if (password.equals("")) {
        	password = " ";
        }
        
        try {
        	String cn = getCommonName(name);
            if (cn != null) {
            	Properties userProp = (Properties)mAdminEnv.clone();
                String dn = CN + '=' + cn + ",ou=people," + mstLdapRoot;
            	userProp.setProperty(Context.SECURITY_PRINCIPAL, dn);             
        		userProp.setProperty(Context.SECURITY_CREDENTIALS, password);
                DirContext userContext = new InitialDirContext(userProp);
                userContext.close(); 
                return true;
            }
        }
        catch(NamingException ex) {	
        }
        return false;
    }
    
    
    /**
     * Save user
     */
    public synchronized void save(User user) throws NamingException {
    	if (doesExist(user.getName())) {
    		update(user);
        }
        else {
        	add(user);
        }
    }
    
    
    /**
     * Add a new user
     */
    private synchronized void add(User user) throws NamingException {
    	
        // empty password is not allowed
        if (user.getPassword() == null) {
        	user.setPassword(" ");
        }
        if (user.getPassword().equals("")) {
        	user.setPassword(" ");
        }
        
        String cn = user.getName() + "-" + System.currentTimeMillis();
    	String path = CN + '=' + cn + ",ou=people," + mstLdapRoot;
        
        Attributes attrs = new BasicAttributes(true);
        attrs.put(new BasicAttribute(LOGIN, user.getName()));
        attrs.put(new BasicAttribute(UID, user.getName()));
        attrs.put(new BasicAttribute(CN, cn));
        attrs.put(new BasicAttribute(SN, user.getName()));
    	attrs.put(new BasicAttribute(PASSWORD, user.getPassword()));
    	attrs.put(OBJCLASS_ATTR);
        attrs.put(new BasicAttribute(ENABLE, String.valueOf(user.getEnabled())));
        attrs.put(new BasicAttribute(ROOT_DIR, user.getVirtualDirectory().getRootDirectory()));
        attrs.put(new BasicAttribute(WRITE_PERM, String.valueOf(user.getVirtualDirectory().getWritePermission())));
        attrs.put(new BasicAttribute(IDLE_TIME, String.valueOf(user.getMaxIdleTime())));
        attrs.put(new BasicAttribute(UP_RATE, String.valueOf(user.getMaxUploadRate())));
        attrs.put(new BasicAttribute(DOWN_RATE, String.valueOf(user.getMaxDownloadRate())));
        
        mAdminContext.bind(path, null, attrs);
    }
    
    
    /**
     * Update an existing user
     */
    private synchronized void update(User user) throws NamingException {
    	String cn = getCommonName(user.getName());
        String dn = CN + '=' + cn + ",ou=people," + mstLdapRoot;
        ArrayList mods = new ArrayList();
        
        if (user.getPassword() != null) {
        	if (user.getPassword().equals("")) {
        		user.setPassword(" ");
            }
        	mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(PASSWORD, user.getPassword())));
        }
        mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(ENABLE, String.valueOf(user.getEnabled()))));
        mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(ROOT_DIR, user.getVirtualDirectory().getRootDirectory())));
        mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(WRITE_PERM, String.valueOf(user.getVirtualDirectory().getWritePermission()))));
    	mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(IDLE_TIME, String.valueOf(user.getMaxIdleTime()))));
        mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(UP_RATE, String.valueOf(user.getMaxUploadRate()))));
    	mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(DOWN_RATE, String.valueOf(user.getMaxDownloadRate()))));
    	
        
        ModificationItem modArr[] = new ModificationItem[mods.size()];
        for(int i=0; i<modArr.length; i++) {
            modArr[i] = (ModificationItem)mods.get(i);
        }
        mAdminContext.modifyAttributes(dn, modArr);
    }
    
    
    /**
     * User existance check
     */
    public synchronized boolean doesExist(String name) {
    	String cn = null;
        try {
        	cn = getCommonName(name);
        }
        catch(NamingException ex) {
        	getConfig().getLogger().error(ex);
        }
        return cn != null;
    }
    
    
    /**
     * Delete user
     */
    public synchronized void delete(String userName) throws NamingException {
    	String cn = getCommonName(userName);
        if (cn != null) {
        	String dn = CN + '=' + cn + ",ou=people," + mstLdapRoot;
            mAdminContext.unbind(dn);
        }
    }
    
    
    /**
     * Close user manager
     */
    public synchronized void dispose() {
        if (mAdminContext != null) {
            try {
                mAdminContext.close();
            }
            catch(NamingException ex) {
            }
            mAdminContext = null;
        }
    }
        
    // static block
    static {
        OBJCLASS_ATTR.add("top");
        OBJCLASS_ATTR.add("person");
        OBJCLASS_ATTR.add("organizationalPerson");
        OBJCLASS_ATTR.add("inetOrgPerson");
        OBJCLASS_ATTR.add("ftpUsers");
    }
}    

